﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Collections.Generic;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    using GetOriginalInboundArticlesFiltered =
            System.Func<Database, Group, bool, bool, System.Linq.IQueryable<IInboundArticle>>;

    using GetThreadCountFiltered = System.Func<Database, Group, bool, bool, int>;
    using Predicate = System.Func<Database, InboundArticle, bool, bool, bool>;

    /// <summary>Represents the single root entity that all host entities refer to.</summary>
    /// <remarks>
    /// <para>Contains a reference to <see cref="Phuse.Net.Forums.Database"/>, which is set in
    /// <see cref="Phuse.Net.Forums.Database.Database(Cache, FileIOProvider, ICryptoProvider)"/>. This allows us to
    /// get the <see cref="Phuse.Net.Forums.Database"/> object from every entity.</para>
    /// <para>Moreover, this class also provides all services needed by entities.</para>
    /// </remarks>
    internal partial class Root
    {
        private Database database;
        private int minEncryptedLength;

        private readonly Dictionary<Delegate, object> resultCache = new Dictionary<Delegate, object>(16);
        private readonly Dictionary<InboundArticleFilter, GetOriginalInboundArticlesFiltered> getOriginalInboundArticlesFilteredMap =
            new Dictionary<InboundArticleFilter, GetOriginalInboundArticlesFiltered>(8);

        private readonly Dictionary<InboundArticleFilter, GetThreadCountFiltered> getThreadCountFilteredMap =
            new Dictionary<InboundArticleFilter, GetThreadCountFiltered>(8);

        private readonly Dictionary<Delegate, int> columnMaxLengthCache = new Dictionary<Delegate, int>(16);

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal void Init(Database newDatabase)
        {
            this.database = newDatabase;
            this.minEncryptedLength = newDatabase.CryptoProvider.Encrypt(new byte[0]).Length;

            try
            {
                this.MaxInboundArticleId =
                    this.Database.InboundArticleTable.Select(a => a.Id).OrderByDescending(id => id).FirstOrDefault();
            }
            catch (InvalidCastException ex)
            {
                throw GetIncompatibleException(ex);
            }
            catch (Exception ex)
            {
                HandleSqlCeException(ex);
                throw;
            }
        }

        internal FileIOProvider FileIOProvider
        {
            get { return this.Database.FileIOProvider; }
        }

        internal ICryptoProvider CryptoProvider
        {
            get { return this.Database.CryptoProvider; }
        }

        internal IQueryable<IOutboundArticle> GetOutboundArticles(SendStatus sendStatus)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, SendStatus status, bool isDeleted) =>
                    from article in db.OutboundArticleTable
                    where (article.SendStatus == status) && (article.IsDeleted == isDeleted)
                    orderby article.DateUtc descending
                    select (IOutboundArticle)article));
            return query(this.Database, sendStatus, false);
        }

        internal IQueryable<KeyValuePair<SendStatus, int>> GetOutboundArticleCounts()
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, bool isDeleted) =>
                    from article in db.OutboundArticleTable
                    where article.IsDeleted == isDeleted
                    group article by article.SendStatus into g
                    select new KeyValuePair<SendStatus, int>(g.Key, g.Count())));
            return query(this.Database, false);
        }

        internal IQueryable<IOutboundArticle> GetDeletedOutboundArticles()
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, bool isDeleted) =>
                    from article in db.OutboundArticleTable
                    where (article.IsDeleted == isDeleted)
                    orderby article.DateUtc descending
                    select (IOutboundArticle)article));
            return query(this.Database, true);
        }

        internal IOrderedQueryable<OutboundArticle> GetOutboundArticles(Host host, SendStatus sendStatus)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, Host h, SendStatus status, bool isDeleted) =>
                    from article in db.OutboundArticleTable
                    where (article.Host == h) && (article.SendStatus == status) && (article.IsDeleted == isDeleted)
                    orderby article.DateUtc
                    select article));
            return query(this.Database, host, sendStatus, false);
        }

        internal IQueryable<Group> GetGroups(Host host)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, Host h) => db.GroupTable.Where(g => g.Host == h)));
            return query(this.Database, host);
        }

        internal IQueryable<InboundArticle> GetContentsToReceive(Group group, int batchSize)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, Group g, bool receiveContent, int size) =>
                    GetCompiledContentsToReceive()(db, g, receiveContent).Take(size)));
            return query(this.Database, group, true, batchSize);
        }

        internal int GetContentsToReceiveCount(Group group)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, Group g, bool receiveContent) =>
                    GetCompiledContentsToReceive()(db, g, receiveContent).Count()));
            return query(this.Database, group, true);
        }

        internal IQueryable<InboundArticle> GetInboundArticles(Group theGroup)
        {
            // The following query has superfluous predicates and orderby columns, which were added to convince
            // SQL Server Compact to use the correct index and refrain from sorting again after reading from the index.
            return
                from article in this.Database.InboundArticleTable
                where (article.Group == theGroup) && (article.Id > 0)
                orderby article.GroupId, article.Id
                select article;
        }

        internal IQueryable<InboundArticle> GetInboundArticlesDescending(Group theGroup)
        {
            // The following query has superfluous predicates and orderby columns, which were added to convince
            // SQL Server Compact to use the correct index and refrain from sorting again after reading from the index.
            return
                from article in this.Database.InboundArticleTable
                where (article.Group == theGroup) && (article.Id > 0)
                orderby article.GroupId, article.Id descending
                select article;
        }

        internal IQueryable<InboundArticle> GetInboundArticlesReferencingOldOnes(Group theGroup, int lastOldArticleId)
        {
            return
                from article in this.Database.InboundArticleTable
                where (article.Group == theGroup) && (article.ReferencedId <= lastOldArticleId)
                orderby article.GroupId, article.ReferencedId
                select article;
        }

        internal Dictionary<string, InboundArticleIds> GetMessageIdMap(Group theGroup)
        {
            // The following query has superfluous predicates and orderby columns, which were added to convince
            // SQL Server Compact to use the correct index and refrain from sorting again after reading from the index.
            var query =
                from article in this.Database.InboundArticleTable
                where (article.Group == theGroup) && (article.Id > 0)
                orderby article.GroupId, article.Id descending
                select new KeyValuePair<string, InboundArticleIds>(
                    article.MessageId, new InboundArticleIds() { Id = article.Id, OriginalId = article.OriginalId });
            return query.ToDictionary(a => a.Key, a => a.Value);
        }

        internal IQueryable<IInboundArticle> GetOriginalInboundArticles(Group group, InboundArticleFilter filter)
        {
            if (filter == InboundArticleFilter.NullFilter)
            {
                var query = this.GetCachedResult(
                    () => CompiledQuery.Compile((Database db, Group g) =>
                        from article in db.InboundArticleTable
                        where (article.Group == g) && (article.ReferencedArticle == null)
                        orderby article.Id descending
                        select (IInboundArticle)article));
                return query(this.Database, group);
            }
            else
            {
                GetOriginalInboundArticlesFiltered getOriginalInboundArticlesFiltered;

                if (!this.getOriginalInboundArticlesFilteredMap.TryGetValue(filter, out getOriginalInboundArticlesFiltered))
                {
                    getOriginalInboundArticlesFiltered = GetCompiledOriginalInboundArticles(filter);
                    this.getOriginalInboundArticlesFilteredMap.Add(filter, getOriginalInboundArticlesFiltered);
                }

                return getOriginalInboundArticlesFiltered(this.Database, group, false, true);
            }
        }

        internal int GetOriginalInboundArticleCount(Group group, InboundArticleFilter filter)
        {
            if (filter == InboundArticleFilter.NullFilter)
            {
                var query = this.GetCachedResult(
                    () => CompiledQuery.Compile((Database db, Group g) =>
                        (from article in db.InboundArticleTable
                         where (article.Group == g) && (article.ReferencedArticle == null)
                         select article.Id).Count()));
                return query(this.Database, group);
            }
            else
            {
                GetThreadCountFiltered getThreadCountFiltered;

                if (!this.getThreadCountFilteredMap.TryGetValue(filter, out getThreadCountFiltered))
                {
                    getThreadCountFiltered = GetCompiledOriginalInboundArticleCount(filter);
                    this.getThreadCountFilteredMap.Add(filter, getThreadCountFiltered);
                }

                return getThreadCountFiltered(this.Database, group, false, true);
            }
        }

        internal IQueryable<InboundArticle> GetFollowupInboundArticles(InboundArticle referencedArticle)
        {
            var query = this.GetCachedResult(
                () => CompiledQuery.Compile((Database db, InboundArticle a) =>
                    from article in db.InboundArticleTable
                    where (article.ReferencedArticle == a)
                    orderby article.ReferencedId, article.Id
                    select article));
            return query(this.Database, referencedArticle);
        }

        internal int HostNameMaxLength
        {
            get { return this.GetColumnMaxLength<Host, string>(() => host => host.Name); }
        }

        internal int UserNameMaxLength
        {
            get { return this.GetColumnMaxLength<Host, string>(() => host => host.UserName); }
        }

        internal int EmailAddressMaxLength
        {
            get { return this.GetColumnMaxLength<Host, string>(() => host => host.EmailAddress); }
        }

        internal int AccountNameMaxLength
        {
            get
            {
                return this.GetColumnMaxLength<Host, byte[]>(() => host => host.AccountNameEncrypted) -
                    this.minEncryptedLength;
            }
        }

        internal int PasswordMaxLength
        {
            get
            {
                return this.GetColumnMaxLength<Host, byte[]>(() => host => host.PasswordEncrypted) -
                    this.minEncryptedLength;
            }
        }

        internal int OutboundArticleSubjectMaxLength
        {
            get { return this.GetColumnMaxLength<OutboundArticle, string>(() => article => article.Subject); }
        }

        internal int GroupNameMaxLength
        {
            get { return this.GetColumnMaxLength<Group, string>(() => group => group.Name); }
        }

        internal int GroupDisplayNameMaxLength
        {
            get { return this.GetColumnMaxLength<Group, string>(() => group => group.DisplayName); }
        }

        internal int InboundArticleFromMaxLength
        {
            get { return this.GetColumnMaxLength<InboundArticle, string>(() => article => article.From); }
        }

        internal int InboundArticleSubjectMaxLength
        {
            get { return this.GetColumnMaxLength<InboundArticle, string>(() => article => article.Subject); }
        }

        internal int InboundArticleMessageIdMaxLength
        {
            get { return this.GetColumnMaxLength<InboundArticle, string>(() => article => article.MessageId); }
        }

        internal int MaxInboundArticleId { get; set; }

        internal void AssertValid()
        {
            this.database.AssertNotDisposedOtherObjects();
        }

        internal void AssertSendReceiveNotInProgress()
        {
            this.Database.AssertSendReceiveNotInProgress();
        }

        internal void InsertOnSubmit<TEntity>(TEntity entity) where TEntity : class
        {
            this.Database.InsertOnSubmit(entity);
        }

        internal void Attach<TEntity>(TEntity entity) where TEntity : class
        {
            this.Database.Attach(entity);
        }

        internal void Attach<TEntity>(TEntity entity, TEntity original) where TEntity : class
        {
            this.Database.Attach(entity, original);
        }

        internal void DeleteOnSubmit<TEntity>(TEntity entity) where TEntity : class
        {
            this.Database.DeleteOnSubmit(entity);
        }

        internal TEntity GetOriginalEntityState<TEntity>(TEntity entity) where TEntity : class
        {
            return this.Database.GetTableFast<TEntity>().GetOriginalEntityState(entity);
        }

        internal void ScheduleAfterCommitAction(Action action)
        {
            this.Database.ScheduleAfterCommitAction(action);
        }

        internal void ReopenDatabaseAfterCommit()
        {
            this.Database.SetReopenAfterCommit();
        }

        internal void SubmitChanges()
        {
            this.Database.SubmitChanges();
        }

        internal static void HandleSqlCeException(Exception ex)
        {
            // On the phone we get a nice SqlCeException, just like on the desktop. Problem is, we don't have access
            // to that type and (due to partial trust on the phone) cannot even get the NativeError property through
            // reflection. So it seems the only way how we can distinguish between the different errors is by
            // switching on ex.Message.
            if (ex.GetType().Name == "SqlCeException")
            {
                switch (ex.Message.Substring(0, 20))
                {
                    case "The specified table ":
                    case "The column name is n":
                        throw GetIncompatibleException(ex);
                    case "The specified locale":
                        throw new IncompatibleCacheException("The database file is corrupt.");
                }
            }
        }

        internal static IncompatibleCacheException GetIncompatibleException(Exception innerException)
        {
            return new IncompatibleCacheException("The database has an unexpected schema.", innerException);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private Database Database
        {
            get
            {
                this.database.AssertNotDisposedOtherObjects();
                return this.database;
            }
        }

        private T GetCachedResult<T>(Func<T> function)
        {
            object result;

            if (!this.resultCache.TryGetValue(function, out result))
            {
                result = function();
                this.resultCache.Add(function, result);
            }

            return (T)result;
        }

        private int GetColumnMaxLength<TType, TPropertyType>(
            Func<Expression<Func<TType, TPropertyType>>> getPropertyExpression)
        {
            int result;

            if (!this.columnMaxLengthCache.TryGetValue(getPropertyExpression, out result))
            {
                var propertyInfo = (PropertyInfo)((MemberExpression)getPropertyExpression().Body).Member;
                var query = this.GetCachedResult(
                    () => CompiledQuery.Compile((Database db, string tableName, string columnName) =>
                        (from schemaColumn in db.GetTable<SchemaColumn>()
                         where (schemaColumn.TableName == tableName) && (schemaColumn.ColumnName == columnName)
                         select (int)schemaColumn.CharacterMaxLength).Single()));
                result = query(this.Database, propertyInfo.DeclaringType.Name, propertyInfo.Name);
                this.columnMaxLengthCache.Add(getPropertyExpression, result);
            }

            return result;
        }

        private static Func<Database, Group, bool, IOrderedQueryable<InboundArticle>> GetCompiledContentsToReceive()
        {
            // The following query has superfluous predicates and orderby columns, which were added to convince
            // SQL Server Compact to use the correct index and refrain from sorting again after reading from the index.
            return
                CompiledQuery.Compile((Database db, Group g, bool receiveContent) =>
                    from article in db.InboundArticleTable
                    where (article.Group == g) && (article.ReceiveContent == receiveContent)
                    orderby article.GroupId, article.ReceiveContent, article.Number
                    select article);
        }

        private static GetOriginalInboundArticlesFiltered GetCompiledOriginalInboundArticles(
            InboundArticleFilter filter)
        {
            Predicate isOriginalBeingWatched;
            Predicate hasOriginalBeenRead;
            Predicate isFollowupBeingWatched;
            Predicate hasFollowupBeenRead;

            GetPredicates(
                filter,
                out isOriginalBeingWatched,
                out hasOriginalBeenRead,
                out isFollowupBeingWatched,
                out hasFollowupBeenRead);

            return CompiledQuery.Compile(
                (Database database, Group theGroup, bool falseValue, bool trueValue) =>
                    from article in database.InboundArticleTable
                    join id in
                        (from original in database.InboundArticleTable
                         where
                            (original.Group == theGroup) && (original.OriginalId == null) &&
                            isOriginalBeingWatched(database, original, falseValue, trueValue) &&
                            hasOriginalBeenRead(database, original, falseValue, trueValue)
                         select original.Id).Union(
                            from followup in database.InboundArticleTable
                            where
                                (followup.Group == theGroup) && (followup.OriginalId != null) &&
                                isFollowupBeingWatched(database, followup, falseValue, trueValue) &&
                                hasFollowupBeenRead(database, followup, falseValue, trueValue)
                            select (int)followup.OriginalId)
                    on article.Id equals id
                    orderby article.Id descending
                    select (IInboundArticle)article);
        }

        private static GetThreadCountFiltered GetCompiledOriginalInboundArticleCount(InboundArticleFilter filter)
        {
            Predicate isOriginalBeingWatched;
            Predicate hasOriginalBeenRead;
            Predicate isFollowupBeingWatched;
            Predicate hasFollowupBeenRead;

            GetPredicates(
                filter,
                out isOriginalBeingWatched,
                out hasOriginalBeenRead,
                out isFollowupBeingWatched,
                out hasFollowupBeenRead);

            return CompiledQuery.Compile((Database database, Group theGroup, bool falseValue, bool trueValue) =>
                (from original in database.InboundArticleTable
                 where
                     (original.Group == theGroup) && (original.OriginalId == null) &&
                     isOriginalBeingWatched(database, original, falseValue, trueValue) &&
                     hasOriginalBeenRead(database, original, falseValue, trueValue)
                 select original.Id).Union(
                     from followup in database.InboundArticleTable
                     where
                         (followup.Group == theGroup) && (followup.OriginalId != null) &&
                         isFollowupBeingWatched(database, followup, falseValue, trueValue) &&
                         hasFollowupBeenRead(database, followup, falseValue, trueValue)
                     select (int)followup.OriginalId).Count());
        }

        private static void GetPredicates(
            InboundArticleFilter filter,
            out Predicate isOriginalBeingWatched,
            out Predicate hasOriginalBeenRead,
            out Predicate isFollowupBeingWatched,
            out Predicate hasFollowupBeenRead)
        {
            // Queries sent to SQL Server Compact will only use indexes containing bit columns if the bit columns are
            // tested for equality against either BIT typed SQL parameters or literals that are explicitly converted to
            // BIT, e.g. CONVERT(BIT, 0). It seems the latter is impossible to get with LINQ to SQL, so we must force
            // the use of SQL parameters with the falseValue and trueValue parameters below. As their names suggest,
            // the values passed for them is always false and true respecively.
            var none = CompiledQuery.Compile(
                (Database db, InboundArticle a, bool falseValue, bool trueValue) => true);
            var isNotBeingWatched = CompiledQuery.Compile(
                (Database db, InboundArticle a, bool falseValue, bool trueValue) => a.IsBeingWatched == falseValue);
            var isBeingWatched = CompiledQuery.Compile(
                (Database db, InboundArticle a, bool falseValue, bool trueValue) => a.IsBeingWatched == trueValue);
            var hasNotBeenRead = CompiledQuery.Compile(
                (Database db, InboundArticle a, bool falseValue, bool trueValue) => a.HasBeenRead == falseValue);
            var hasBeenRead = CompiledQuery.Compile(
                (Database db, InboundArticle a, bool falseValue, bool trueValue) => a.HasBeenRead == trueValue);

            isOriginalBeingWatched =
                GetPredicate(filter.IsOriginalBeingWatched, none, isNotBeingWatched, isBeingWatched);
            hasOriginalBeenRead = GetPredicate(filter.HasOriginalBeenRead, none, hasNotBeenRead, hasBeenRead);
            isFollowupBeingWatched =
                GetPredicate(filter.IsFollowupBeingWatched, none, isNotBeingWatched, isBeingWatched);
            hasFollowupBeenRead = GetPredicate(filter.HasFollowupBeenRead, none, hasNotBeenRead, hasBeenRead);
        }

        private static Predicate GetPredicate(bool? filter, Predicate none, Predicate equalsFalse, Predicate equalsTrue)
        {
            switch (filter)
            {
                case null:
                    return none;
                case false:
                    return equalsFalse;
                default:
                    return equalsTrue;
            }
        }
    }
}
